Passed
Push — master ( 6fbad5...2f62e3 )
by Dave
01:25
created

eventsource.js ➔ ... ➔ connect   F

Complexity

Conditions 17
Paths 6144

Size

Total Lines 155
Code Lines 93

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 17
eloc 93
c 1
b 0
f 1
nc 6144
dl 0
loc 155
rs 1.489
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like eventsource.js ➔ ... ➔ connect often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
const original = require('original');
2
const parse = require('url').parse;
3
const events = require('events');
4
const https = require('https');
5
const http = require('http');
6
const util = require('util');
7
8
const httpsOptions = [
9
  'pfx', 'key', 'passphrase', 'cert', 'ca', 'ciphers',
10
  'rejectUnauthorized', 'secureProtocol', 'servername',
11
];
12
13
/**
14
 * Creates a new EventSource object
15
 *
16
 * @param {String} url the URL to which to connect
17
 * @param {Object} [eventSourceInitDict] extra init params. See README for details.
18
 * @api public
19
 * */
20
function EventSource(url, eventSourceInitDict) {
21
  let readyState = EventSource.CONNECTING;
22
  Object.defineProperty(this, 'readyState', {
23
    get() {
24
      return readyState;
25
    },
26
  });
27
28
  Object.defineProperty(this, 'url', {
29
    get() {
30
      return url;
31
    },
32
  });
33
34
  const self = this;
35
  self.reconnectInterval = 60000;
36
37
  function onConnectionClosed() {
38
    if (readyState === EventSource.CLOSED) return;
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
39
    readyState = EventSource.CONNECTING;
40
    _emit('error', new Event('error'));
41
42
    // The url may have been changed by a temporary
43
    // redirect. If that's the case, revert it now.
44
    if (reconnectUrl) {
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable reconnectUrl is declared in the current environment, consider using typeof reconnectUrl === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
45
      url = reconnectUrl;
46
      reconnectUrl = null;
0 ignored issues
show
Bug introduced by
The local (let) variable reconnectUrl is used before it is defined. This will cause a reference error.
Loading history...
47
    }
48
    setTimeout(() => {
49
      if (readyState !== EventSource.CONNECTING) {
50
        return;
51
      }
52
      connect();
53
    }, self.reconnectInterval);
54
  }
55
56
  let req;
57
  let lastEventId = '';
58
  if (eventSourceInitDict && eventSourceInitDict.headers && eventSourceInitDict.headers['Last-Event-ID']) {
59
    lastEventId = eventSourceInitDict.headers['Last-Event-ID'];
60
    delete eventSourceInitDict.headers['Last-Event-ID'];
61
  }
62
63
  let discardTrailingNewline = false;
64
  let data = '';
65
  let eventName = '';
66
67
  let reconnectUrl = null;
0 ignored issues
show
Unused Code introduced by
The variable reconnectUrl seems to be never used. Consider removing it.
Loading history...
68
69
  function connect() {
70
    const options = parse(url);
71
    let isSecure = options.protocol === 'https:';
72
    options.headers = { 'Cache-Control': 'no-cache', Accept: 'text/event-stream' };
73
    if (lastEventId) options.headers['Last-Event-ID'] = lastEventId;
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
74
    if (eventSourceInitDict && eventSourceInitDict.headers) {
75
      for (const i in eventSourceInitDict.headers) {
76
        const header = eventSourceInitDict.headers[i];
77
        if (header) {
78
          options.headers[i] = header;
79
        }
80
      }
81
    }
82
83
    // Legacy: this should be specified as `eventSourceInitDict.https.rejectUnauthorized`,
84
    // but for now exists as a backwards-compatibility layer
85
    options.rejectUnauthorized = !(eventSourceInitDict && !eventSourceInitDict.rejectUnauthorized);
86
87
    // If specify http proxy, make the request to sent to the proxy server,
88
    // and include the original url in path and Host headers
89
    const useProxy = eventSourceInitDict && eventSourceInitDict.proxy;
90
    if (useProxy) {
91
      const proxy = parse(eventSourceInitDict.proxy);
92
      isSecure = proxy.protocol === 'https:';
93
94
      options.protocol = isSecure ? 'https:' : 'http:';
95
      options.path = url;
96
      options.headers.Host = options.host;
97
      options.hostname = proxy.hostname;
98
      options.host = proxy.host;
99
      options.port = proxy.port;
100
    }
101
102
    // If https options are specified, merge them into the request options
103
    if (eventSourceInitDict && eventSourceInitDict.https) {
104
      for (const optName in eventSourceInitDict.https) {
105
        if (httpsOptions.indexOf(optName) === -1) {
106
          continue;
107
        }
108
109
        const option = eventSourceInitDict.https[optName];
110
        if (option !== undefined) {
111
          options[optName] = option;
112
        }
113
      }
114
    }
115
116
    // Pass this on to the XHR
117
    if (eventSourceInitDict && eventSourceInitDict.withCredentials !== undefined) {
118
      options.withCredentials = eventSourceInitDict.withCredentials;
119
    }
120
121
    req = (isSecure ? https : http).request(options, (res) => {
122
      // Handle HTTP errors
123
      if (res.statusCode === 500 || res.statusCode === 502
124
        || res.statusCode === 503 || res.statusCode === 504) {
125
        _emit('error', new Event('error', { status: res.statusCode }));
126
        onConnectionClosed();
127
        return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
128
      }
129
130
      // Handle HTTP redirects
131
      if (res.statusCode === 301 || res.statusCode === 307) {
132
        if (!res.headers.location) {
133
          // Server sent redirect response without Location header.
134
          _emit('error', new Event('error', { status: res.statusCode }));
135
          return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
136
        }
137
        if (res.statusCode === 307) reconnectUrl = url;
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
Unused Code introduced by
The variable reconnectUrl seems to be never used. Consider removing it.
Loading history...
138
        url = res.headers.location;
139
        process.nextTick(connect);
140
        return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
141
      }
142
143
      if (res.statusCode !== 200) {
144
        _emit('error', new Event('error', { status: res.statusCode }));
145
        return self.close();
146
      }
147
148
      // protect against multiple connects
149
      // https://github.com/tigertext/eventsource/commit/ca8a6e0ca0db10c23ba7bf2b7f8affaa23d7a265
150
      if (readyState === EventSource.OPEN) {
151
        return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
152
      }
153
154
      readyState = EventSource.OPEN;
155
      res.on('close', () => {
156
        res.removeAllListeners('close');
157
        res.removeAllListeners('end');
158
        onConnectionClosed();
159
      });
160
161
      res.on('end', () => {
162
        res.removeAllListeners('close');
163
        res.removeAllListeners('end');
164
        onConnectionClosed();
165
      });
166
      _emit('open', new Event('open'));
167
168
      // text/event-stream parser adapted from webkit's
169
      // Source/WebCore/page/EventSource.cpp
170
      let buf = '';
171
      res.on('data', (chunk) => {
172
        buf += chunk;
173
174
        let pos = 0;
175
        const length = buf.length;
176
177
        while (pos < length) {
178
          if (discardTrailingNewline) {
0 ignored issues
show
Bug introduced by
The variable discardTrailingNewline is changed as part of the while loop for example by false on line 182. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
179
            if (buf[pos] === '\n') {
180
              ++pos;
181
            }
182
            discardTrailingNewline = false;
183
          }
184
185
          let lineLength = -1;
186
          let fieldLength = -1;
187
          var c;
188
189
          for (let i = pos; lineLength < 0 && i < length; ++i) {
190
            c = buf[i];
191
            if (c === ':') {
192
              if (fieldLength < 0) {
193
                fieldLength = i - pos;
194
              }
195
            } else if (c === '\r') {
196
              discardTrailingNewline = true;
197
              lineLength = i - pos;
198
            } else if (c === '\n') {
199
              lineLength = i - pos;
200
            }
201
          }
202
203
          if (lineLength < 0) {
204
            break;
205
          }
206
207
          parseEventStreamLine(buf, pos, fieldLength, lineLength);
208
209
          pos += lineLength + 1;
210
        }
211
212
        if (pos === length) {
213
          buf = '';
214
        } else if (pos > 0) {
215
          buf = buf.slice(pos);
216
        }
217
      });
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
218
    });
219
220
    req.on('error', onConnectionClosed);
221
    if (req.setNoDelay) req.setNoDelay(true);
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
222
    req.end();
223
  }
224
225
  connect();
226
227
  function _emit() {
228
    if (self.listeners(arguments[0]).length > 0) {
229
      self.emit(...arguments);
230
    }
231
  }
232
233
  this._close = function () {
234
    if (readyState === EventSource.CLOSED) return;
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
235
    readyState = EventSource.CLOSED;
236
    if (req.abort) req.abort();
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
237
    if (req.xhr && req.xhr.abort) req.xhr.abort();
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
238
  };
239
240
  function parseEventStreamLine(buf, pos, fieldLength, lineLength) {
241
    if (lineLength === 0) {
242
      if (data.length > 0) {
243
        const type = eventName || 'message';
244
        _emit(type, new MessageEvent(type, {
245
          data: data.slice(0, -1), // remove trailing newline
246
          lastEventId,
247
          origin: original(url),
248
        }));
249
        data = '';
250
      }
251
      eventName = void 0;
0 ignored issues
show
Coding Style introduced by
Consider using undefined instead of void(0). It is equivalent and more straightforward to read.
Loading history...
252
    } else if (fieldLength > 0) {
253
      const noValue = fieldLength < 0;
254
      let step = 0;
255
      const field = buf.slice(pos, pos + (noValue ? lineLength : fieldLength));
256
257
      if (noValue) {
258
        step = lineLength;
259
      } else if (buf[pos + fieldLength + 1] !== ' ') {
260
        step = fieldLength + 1;
261
      } else {
262
        step = fieldLength + 2;
263
      }
264
      pos += step;
265
266
      const valueLength = lineLength - step;
267
      const value = buf.slice(pos, pos + valueLength);
268
269
      if (field === 'data') {
270
        data += `${value}\n`;
271
      } else if (field === 'event') {
272
        eventName = value;
273
      } else if (field === 'id') {
274
        lastEventId = value;
275
      } else if (field === 'retry') {
276
        const retry = parseInt(value, 10);
277
        if (!Number.isNaN(retry)) {
278
          self.reconnectInterval = retry;
279
        }
280
      }
281
    }
282
  }
283
}
284
285
module.exports = EventSource;
286
287
util.inherits(EventSource, events.EventEmitter);
288
EventSource.prototype.constructor = EventSource; // make stacktraces readable
289
290
['open', 'error', 'message'].forEach((method) => {
291
  Object.defineProperty(EventSource.prototype, `on${method}`, {
292
    /**
293
     * Returns the current listener
294
     *
295
     * @return {Mixed} the set function or undefined
296
     * @api private
297
     */
298
    get: function get() {
299
      const listener = this.listeners(method)[0];
300
      return listener ? (listener._listener ? listener._listener : listener) : undefined;
301
    },
302
303
    /**
304
     * Start listening for events
305
     *
306
     * @param {Function} listener the listener
307
     * @return {Mixed} the set function or undefined
308
     * @api private
309
     */
310
    set: function set(listener) {
311
      this.removeAllListeners(method);
312
      this.addEventListener(method, listener);
313
    },
314
  });
315
});
316
317
/**
318
 * Ready states
319
 */
320
Object.defineProperty(EventSource, 'CONNECTING', { enumerable: true, value: 0 });
321
Object.defineProperty(EventSource, 'OPEN', { enumerable: true, value: 1 });
322
Object.defineProperty(EventSource, 'CLOSED', { enumerable: true, value: 2 });
323
324
EventSource.prototype.CONNECTING = 0;
325
EventSource.prototype.OPEN = 1;
326
EventSource.prototype.CLOSED = 2;
327
328
/**
329
 * Closes the connection, if one is made, and sets the readyState attribute to 2 (closed)
330
 *
331
 * @see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close
332
 * @api public
333
 */
334
EventSource.prototype.close = function () {
335
  this._close();
336
};
337
338
/**
339
 * Emulates the W3C Browser based WebSocket interface using addEventListener.
340
 *
341
 * @param {String} type A string representing the event type to listen out for
342
 * @param {Function} listener callback
343
 * @see https://developer.mozilla.org/en/DOM/element.addEventListener
344
 * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
345
 * @api public
346
 */
347
EventSource.prototype.addEventListener = function addEventListener(type, listener) {
348
  if (typeof listener === 'function') {
349
    // store a reference so we can return the original function again
350
    listener._listener = listener;
351
    this.on(type, listener);
352
  }
353
};
354
355
/**
356
 * Emulates the W3C Browser based WebSocket interface using removeEventListener.
357
 *
358
 * @param {String} type A string representing the event type to remove
359
 * @param {Function} listener callback
360
 * @see https://developer.mozilla.org/en/DOM/element.removeEventListener
361
 * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
362
 * @api public
363
 */
364
EventSource.prototype.removeEventListener = function removeEventListener(type, listener) {
365
  if (typeof listener === 'function') {
366
    listener._listener = undefined;
367
    this.removeListener(type, listener);
368
  }
369
};
370
371
/**
372
 * W3C Event
373
 *
374
 * @see http://www.w3.org/TR/DOM-Level-3-Events/#interface-Event
375
 * @api private
376
 */
377
function Event(type, optionalProperties) {
378
  Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true });
379
  if (optionalProperties) {
380
    for (const f in optionalProperties) {
381
      if (optionalProperties.hasOwnProperty(f)) {
382
        Object.defineProperty(this, f, { writable: false, value: optionalProperties[f], enumerable: true });
383
      }
384
    }
385
  }
386
}
387
388
/**
389
 * W3C MessageEvent
390
 *
391
 * @see http://www.w3.org/TR/webmessaging/#event-definitions
392
 * @api private
393
 */
394
function MessageEvent(type, eventInitDict) {
395
  Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true });
396
  for (const f in eventInitDict) {
397
    if (eventInitDict.hasOwnProperty(f)) {
398
      Object.defineProperty(this, f, {
399
        writable: false, value: eventInitDict[f], enumerable: true
400
      });
401
    }
402
  }
403
}
404